package com.github.livebase.util.html;

import android.graphics.Color;
import android.graphics.Typeface;
import android.text.Editable;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.style.ForegroundColorSpan;
import android.text.style.RelativeSizeSpan;
import android.text.style.StyleSpan;

import com.github.livebase.util.StringKeyValuePairs;
import com.github.livebase.util.base.CharIterator;
import com.github.livebase.util.base.CollectionsUtil;

import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Stack;

/**
 * @ClassName HTMLUtil
 * @description:
 * @author: biao532
 * @create: 2024-03-16 16:20
 * @Version 1.0
 **/
public class HTMLUtil {
    private static final List<String> selfCloseTag = Arrays.asList("br", "hr", "img");
    private static final float[] HEADING_SIZES = {
            1.5f, 1.4f, 1.3f, 1.2f, 1.1f, 1f,
    };

    public static Spanned fromHtml(String source) throws HTMLException {
        Element element = parseHtml(source, "root");
        SpannableStringBuilder builder = new SpannableStringBuilder();
        fromHtml(element, builder);
        return builder;
    }
    public static void fromHtml(Element element, SpannableStringBuilder builder) {
        if (element == null) {
            return ;
        }

        CollectionsUtil.foreach(element.getChildren(), (i, e)-> {
            int where = builder.length();
            fromHtml(e, builder);
            String tagName = e.getName();
            if (e.getClass() == TextElement.class) {
                builder.append(e.text());
            }
            int to = builder.length();
            if ("br".equalsIgnoreCase(tagName)) {
                handleBr(builder);
            }
            else if ("font".equalsIgnoreCase(tagName)) {
                handleFont(e,  where, to, builder);
            }
            else if ("b".equalsIgnoreCase(tagName)) {
                handleStyleSpan(Typeface.BOLD,  where, to, builder);
            }
            else if ("i".equalsIgnoreCase(tagName)) {
                handleStyleSpan(Typeface.ITALIC,  where, to, builder);
            }
            else if ("big".equalsIgnoreCase(tagName)) {
                handleRelativeSizeSpan(1.25f, where, to, builder);
            }
            else if ("small".equalsIgnoreCase(tagName)) {
                handleRelativeSizeSpan(0.8f, where, to, builder);
            }
            else if (tagName.length() == 2 && Character.toLowerCase(tagName.charAt(0)) == 'h' &&
                    tagName.charAt(1) >= '1' && tagName.charAt(1) <= '6') {
                handleRelativeSizeSpan(HEADING_SIZES[tagName.charAt(1) - '1'], where, to, builder);
                handleStyleSpan(Typeface.BOLD, where, to, builder);
            }
        });
    }
    private static void handleFont(Element element, int where, int to, SpannableStringBuilder builder) {
        String color = element.getAttribute("color");
        if (!TextUtils.isEmpty(color)) {
            ForegroundColorSpan span = new ForegroundColorSpan(Color.parseColor(color));
            builder.setSpan(span, where, to ,Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        }
        String size = element.getAttribute("size");
        if (!TextUtils.isEmpty(size)) {
            ForegroundColorSpan span = new ForegroundColorSpan(Color.parseColor(color));
            builder.setSpan(span, where, to ,Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        }
    }
    private static void handleRelativeSizeSpan(float proportion, int where, int to, SpannableStringBuilder builder) {
        RelativeSizeSpan span = new RelativeSizeSpan(proportion);
        builder.setSpan(span, where, to ,Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    }
    private static void handleStyleSpan(int type, int where, int to, SpannableStringBuilder builder) {
        StyleSpan span = new StyleSpan(type);
        builder.setSpan(span, where, to ,Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    }
    private static void handleBr(Editable spannable) {
        spannable.append('\n');
    }
    public static Element parseHtml(String data, String rootName) throws HTMLException {
        Stack<Element> elementStack = new Stack<>();
        elementStack.add(new Element(rootName));
        TagIterator iterator = new TagIterator(data);
        while (iterator.hasNext()) {
            Tag tag = iterator.next();
            if (tag.getType() == Tag.TYPE_TAG_OPEN) {
                elementStack.push(createElement(tag));
                continue;
            }
            if (tag.getType() == Tag.TYPE_TAG_TEXT) {
                elementStack.peek().appendChild(new TextElement(tag.tagName));
                continue;
            }
            if (tag.getType() == Tag.TYPE_TAG_CLOSE) {
                if (isSelfClose(tag)) {
                    elementStack.peek().appendChild(createElement(tag));
                    continue;
                }
                Element element = null;
                while (!elementStack.isEmpty() && !(element = elementStack.pop()).getName().equals(tag.getTagName())) {

                }
                if (!elementStack.isEmpty()) {
                    elementStack.peek().appendChild(element);
                }
                else {
                    throw new HTMLException("Unable to match element " + tag.tagName);
                }
            }
        }
        if (elementStack.size() == 1) {
            return elementStack.pop();
        }
        throw new HTMLException("Elements could not be matched correctly");
    }


    private static boolean isSelfClose(Tag element) {
        return element.getType() == Tag.TYPE_TAG_CLOSE &&
                CollectionsUtil.first(selfCloseTag, (c)-> c.equalsIgnoreCase(element.tagName) ) != null;
    }

    private static Element createElement(Tag tag) {
        if (isSelfClose(tag)) {
            return new SelfCloseElement(tag.tagName);
        }
        Element element = new Element(tag.tagName);
        parseAttribute(tag.getAttr(), element);
        return element;
    }
    private static void parseAttribute(String attr, Element element) {
        if (attr == null || attr.isEmpty()) {
            return;
        }
        CharIterator iterator = new CharIterator(attr);
        String key = null;
        while ( (key = CharIterator.readUntil(iterator, '=', ' ')) != null) {
            if (key.isEmpty()) {
                return;
            }
            char previous = ' ';
            if (iterator.hasPrevious()) {
                previous = iterator.previous();
            }
            if (previous == ' ') {
                continue;
            }
            CharIterator.readUntil(iterator, '"', '\'');
            if (iterator.hasPrevious()) {
                previous = iterator.previous();
                iterator.next();
            }
            String value = CharIterator.readUntil(iterator, previous);
            element.setAttribute(key, value);
            while (iterator.hasNext() && ' ' == iterator.next()) {

            }
            if (iterator.hasNext() && iterator.hasPrevious()) {
                iterator.previous();
            }
        }
    }

    private static class Tag {
        private static final int TYPE_TAG_OPEN = 1;
        private static final int TYPE_TAG_CLOSE = 0;
        private static final int TYPE_TAG_TEXT = 2;
        private int type;
        private String tagName;
        private String attr;

        public Tag(int type, String tagName) {
            this(type, tagName, "");
        }
        public Tag(int type, String tagName, String attr) {
            this.type = type;
            this.tagName = tagName;
            this.attr = attr;
        }

        public int getType() {
            return type;
        }

        public String getTagName() {
            return tagName;
        }

        public String getAttr() {
            return attr;
        }

        @Override
        public String toString() {
            return "Tag{" +
                    "type=" + type +
                    ", data='" + tagName + '\'' +
                    '}';
        }
    }
    private static class TagIterator implements Iterator<Tag> {
        private CharIterator iterator = null;

        private TagIterator(String data) {
            iterator = new CharIterator(data);
        }
        @Override
        public boolean hasNext() {
            return iterator.hasNext();
        }

        @Override
        public Tag next() {
            if (iterator.hasNext() && iterator.next() == '<') {
                if (iterator.next() == '/') {
                    return new Tag(Tag.TYPE_TAG_CLOSE, CharIterator.readUntil(iterator, '>').trim());
                }

                iterator.previous();
                String str = CharIterator.readUntil(iterator, '>');
                if (str.endsWith("/")) {
                    return new Tag(Tag.TYPE_TAG_CLOSE, str.substring(0, str.length()-1).trim());
                }
                StringKeyValuePairs pairs = StringKeyValuePairs.createKeyValuePairs(str, " ");
                return new Tag(Tag.TYPE_TAG_OPEN, pairs.getKey().trim(), pairs.getValue().trim());
            }
            if (iterator.hasPrevious()) {
                iterator.previous();
            }
            Tag tag = new Tag(Tag.TYPE_TAG_TEXT, CharIterator.readUntil(iterator, '<'));
            if (iterator.hasNext() && iterator.hasPrevious()) {
                iterator.previous();
            }
            return tag;
        }
    }
}
